package com.atguigu.gmall.index.aspect;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class GmallCacheAspect {
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    RedissonClient redissonClient;
    //环绕通知: 对使用自定义注解GmallCache的方法进行增强
    @Around("@annotation(com.atguigu.gmall.index.aspect.GmallCache)") //@annotation 对指定注解进行切面 通知
    public Object around(ProceedingJoinPoint joinPoint){//切入点： 可以获取到切入点方法对象 和参数等信息
//        MethodSignatureImpl

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//获取切入点方法信息
        Method method = signature.getMethod();//获取目标方法的对象
        Class returnType = signature.getReturnType();//获取目标方法的返回值类型
        //1、获取目标方法上的注解对象：获取注解对象中的 缓存key  lockkey  过期时间...
        GmallCache gmallCache = method.getAnnotation(GmallCache.class);
        String key = gmallCache.key();
        String lockName = gmallCache.lockName();
        int timeout = gmallCache.timeout();
        int random = gmallCache.random();
        //2、获取目标方法的参数列表
        Object[] args = joinPoint.getArgs();
        //3、判断是否存在缓存
        String cacheKey = key;
        if(ArrayUtils.isNotEmpty(args)){
            //使用布隆过滤器判断查询的数据是否存在：
            //cid查询它的二级分类： cid必须存在 并且它有二级分类集合  这个cid就有数据
            // 项目启动时 可以将存在并且有二级分类的cid存入到布隆过滤器中
            RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("bloom:cates");
            boolean contains = bloomFilter.contains(args[0].toString());
            if(!contains){
                //缓存一定不存在
                log.info("bloomfilter判断数据不存在：{}" ,args[0].toString());
                return null;
            }
            cacheKey = key+":"+StringUtils.join(args,"-");
            lockName = lockName+":"+StringUtils.join(args,"-");
        }
        // redisTemplate配置过键和值的序列化器，所以返回的Object真实类型就是原数据自己的类型
        Object obj = redisTemplate.opsForValue().get(cacheKey);
        if(obj!=null){
            return obj;
        }
        //4、缓存不存在查询数据
        //分布式锁：解决雪崩
        RLock lock = redissonClient.getLock(lockName);
        lock.lock();
        //再次判断是否有缓存
        obj = redisTemplate.opsForValue().get(cacheKey);
        if(obj!=null){
            lock.unlock();//查询到缓存后释放锁
            return obj;
        }
        //5、执行目标方法：查询数据库中的二级分类和子集合数据
        try {
            Object result = joinPoint.proceed(args);
            long cacheTime = timeout+new Random().nextInt(random);
            if(result==null  || (result instanceof List && CollectionUtils.isEmpty((List)result))){
                cacheTime = random;
            }
            //存入缓存
            redisTemplate.opsForValue().set(cacheKey , result ,
                    cacheTime,
                    TimeUnit.SECONDS);
            //空值也存入到缓存中  时间稍短

            return result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            lock.unlock();//释放锁
        }
        return null;
    }

}
